﻿#pragma once

#include "evnet/queue.hpp"
#include "evnet/http/http.hpp"
#include "evnet/http/operation.hpp"

#include <iostream>
#include <cassert>

namespace evnet {
namespace http {

class reactor
{
    queue& queue_;
    multi_handle_type handle_{nullptr};
    int running_{0};
    event timer_;

    static multi_handle_type create()
    {
        multi_handle_type result = curl_multi_init();
        if (!result)
            throw std::runtime_error("curl_multi_init");

        return result;
    }

    void destroy() throw()
    {
        if (handle_)
        {
            curl_multi_cleanup(handle_);
            handle_ = nullptr;
        }
    }

public:

    reactor(queue& queue)
        : queue_(queue)
        , handle_(create())
        , timer_()
    {
        set_option(handle_, CURLMOPT_SOCKETFUNCTION, &reactor::sock_fn);
        set_option(handle_, CURLMOPT_SOCKETDATA, this);
        set_option(handle_, CURLMOPT_TIMERFUNCTION, &reactor::timer_fn);
        set_option(handle_, CURLMOPT_TIMERDATA, this);

        int result = evtimer_assign(&timer_, queue_, &reactor::timer_cb, this);
        if (result != 0)
            throw std::runtime_error("evtimer_assign");
    }

    ~reactor()
    {
        destroy();
    }

    int running() const throw()
    {
        return running_;
    }

    multi_handle_type handle() const throw()
    {
        return handle_;
    }

    queue::handle_type base() throw()
    {
        return queue_.handle();
    }

private:

    void set_timer(const struct timeval& timeout) throw()
    {
        evtimer_add(&timer_, &timeout);
    }

    void kill_timer() throw()
    {
        if (!evtimer_pending(&timer_, 0))
            event_del(&timer_);
    }

    void activate() throw()
    {
        event_active(&timer_, EV_WRITE, 0);
    }

    static void timer_cb(int, short, void *rt)
    {
        reactor& r = *static_cast<reactor*>(rt);
        int running = 0;
        CURLMcode m = curl_multi_socket_action(r.handle(), CURL_SOCKET_TIMEOUT,
                                               0, &running);
        if (m != CURLM_OK)
            throw std::runtime_error(curl_multi_strerror(m));

        r.dispatch(running);
    }

    void update_handle(easy_handle_type handle, CURLcode result)
    {
        CURLMcode m = curl_multi_remove_handle(handle_, handle);
        if (m != CURLM_OK)
            throw std::runtime_error(curl_multi_strerror(m));

        operation *op = nullptr;

        CURLcode e = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &op);

        if (e != CURLE_OK)
            throw std::runtime_error(curl_easy_strerror(e));

        op->done(result);
    }

    /* Check for completed transfers, and remove their easy handles */
    void dispatch(int running)
    {
        int count = 0;
        CURLMsg *msg = nullptr;
        running_ = running;

        while ((msg = curl_multi_info_read(handle_, &count)))
        {
            if (msg->msg == CURLMSG_DONE)
            {
                update_handle(msg->easy_handle, msg->data.result);
            }
        }
    }

    static inline void event_cb(int fd, short kind, void *rt)
    {
        reactor& r = *static_cast<reactor*>(rt);

        int action =
            (kind & EV_READ ? CURL_CSELECT_IN : 0) |
            (kind & EV_WRITE ? CURL_CSELECT_OUT : 0);

        int running = 0;
        CURLMcode m = curl_multi_socket_action(r.handle(), fd,
                                                  action, &running);
        if (m != CURLM_OK)
            throw std::runtime_error(curl_multi_strerror(m));

        r.dispatch(running);

        if (running <= 0)
            r.kill_timer();
    }

    static inline void remove_event(reactor& reactor, curl_socket_t s,
                                    event& event)
    {
        event_free(&event);

        CURLMcode m = curl_multi_assign(reactor.handle(), s, 0);
        if (m != CURLM_OK)
            throw std::runtime_error(curl_multi_strerror(m));
    }


    static inline void set_event(reactor& reactor, curl_socket_t s,
                                 int action, event& event)
    {
        int type = EV_PERSIST | (action & CURL_POLL_IN ? EV_READ : 0) |
            (action & CURL_POLL_OUT ? EV_WRITE : 0);

        if (event_initialized(&event))
            event_del(&event);

        int result = event_assign(&event, reactor.base(), s, type,
                                  event_cb, &reactor);
        if (result != 0)
            throw std::runtime_error("event_assign");

        result = event_add(&event, 0);
        if (result != 0)
            throw std::runtime_error("event_add");
    }

    static inline void add_event(reactor& reactor, curl_socket_t s, int action)
    {
        int type = EV_PERSIST | (action & CURL_POLL_IN ? EV_READ : 0) |
            (action & CURL_POLL_OUT ? EV_WRITE : 0);

        event* e = event_new(reactor.base(), s, type, event_cb, &reactor);
        if (!e)
            throw std::runtime_error("event_new");

        int result = event_add(e, 0);
        if (result != 0)
            throw std::runtime_error("event_add");

        CURLMcode m = curl_multi_assign(reactor.handle(), s, e);
        if (m != CURLM_OK)
            throw std::runtime_error(curl_multi_strerror(m));
    }


    static inline int sock_fn(CURL *, curl_socket_t socket,
                              int type, void *rt, void *ev)
    {
        reactor* r = static_cast<reactor*>(rt);
        event* e = static_cast<event*>(ev);

        if (e)
        {
            if (type == CURL_POLL_REMOVE)
                remove_event(*r, socket, *e);
            else
                set_event(*r, socket, type, *e);
        }
        else
            add_event(*r, socket, type);

        return 0;
    }

	static inline int timer_fn(multi_handle_type, long ms, void *rt)
    {
        reactor& r = *static_cast<reactor*>(rt);
        if (ms == -1)
        {
            r.activate();
            return 0;
        }

        timeval timeout = { ms / 1000, (ms % 1000) * 1000 };
        r.set_timer(timeout);
        return 0;
    }

    reactor(const reactor&) = delete;
    reactor& operator=(const reactor&) = delete;

};

} // namsspace http
} // namespace evnet
